/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package idea.sfo.viewer;


import com.itextpdf.text.DocumentException;
import idea.sfo.viewer.utilites.ToolBar;
import idea.sfo.viewer.utilites.MouseManager;
import idea.sfo.viewer.designer.DesignerColNode;
import idea.sfo.viewer.designer.DesignerElement;
import idea.sfo.viewer.designer.DesignerElementChildren;
import idea.sfo.viewer.designer.DesignerRowChildren;
import idea.sfo.viewer.designer.DesignerRowNode;
import idea.sfo.viewer.graphics.Drawable;
import idea.sfo.viewer.utilites.ImageWriter;
import idea.sfo.viewer.utilites.PdfFileWriter;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.beans.PropertyVetoException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.TreeMap;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.border.EtchedBorder;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;


/**
 *
 * @author renke002
 */
public class Canvas extends JComponent
{

    /*
     ************************STATIC-VARIABLES***********************************
     */
    /**
     * Unit name for a "Fundamental Unit", appended to the end of measurements
     * in strings.
     */
    public final static String FU_UNIT_NAME = "fu";
    /**
     * On screen DPI.
     */
    public final static long FU_SCALE_FACTOR = 76500;
    /**
     * Number of Fundamental (Fenestra) Units in an Inch.
     */
    public final static int FU_TO_INCH_FACTOR = 7342632;
    /**
     * Number of Fundamental (Fenestra) Units in a point.
     * <p/>
     * Where a point is defined as being 72 units (dots) per inch, as used by
     * Adobe's PostScript format and Java.
     */
    public final static int FU_TO_POINT_FACTOR = 101981;
    /**
     * Number of Fundamental (Fenestra) Units in a Printer's Point.
     * <p/>
     * This was defined as 72.27 units per inch, and is useful to approximate
     * AmGraf points (72.3).
     */
    public final static int FU_TO_PP_FACTOR = 101600;
    /*
     *
     ************************INSTANCE-VARIABLES*********************************
     *
     */
    /**
     * Background color, set to white by default.
     */
    private Color bgColor = Color.WHITE;
    /**
     * Mapping between the column number and its width.
     * <p/>
     * Due to the structure of the SFO file, the widths of all the columns are
     * stored as attributes to the last row (of null/empty cells).
     */
    private TreeMap< Integer , Long> colWidths = null;
    /**
     *
     */
    private DesignerElementChildren designerElements;
    /**
     * When true draw the background grid on the canvas.
     */
    private boolean drawGrid;
    /**
     * global shared list that will contain all the object that need to be
     * painted on the screen.
     */
    private ArrayList<Drawable> graphics;
    /**
     * Mouse actions manager.
     */
    private MouseManager mouseMan;
    /**
     * Mapping between the row number and its height.
     */
    private TreeMap< Integer , Long> rowHeights = null;
    /**
     *
     */
    private ToolBar toolBar;
    /**
     * Through our parent TopComponent we have access to the ExplorerManager
     * that contains all the nodes we need to display.
     */
    private SfoViewerTopComponent topComponent;
    /**
     *
     * @Deprecated
     */
    @Deprecated
    private HashMap< DesignerRowNode , HashSet<DesignerColNode>> nodes;
    /**
     *
     */
    private final boolean verbose;


    /**
     *
     */
    public Canvas( ToolBar tb , SfoViewerTopComponent treeTop , boolean verbose )
    {
        //
        //
        this.verbose = verbose;
        /*
         * Instantiate the shapes' list. Drawable
         */
        this.graphics = new ArrayList<>();

        this.setBorder(
                BorderFactory.createEtchedBorder( EtchedBorder.RAISED ) );

        // by default draw the background grid.
        this.drawGrid = false;

        //
        this.nodes = null;

        // 
        this.toolBar = tb;

        //
        this.topComponent = treeTop;


        //
        this.mouseMan = new MouseManager( this.toolBar , this );
        this.addMouseListener( this.mouseMan );
        this.addMouseMotionListener( this.mouseMan );
        this.addMouseWheelListener( this.mouseMan );
    }


    /**
     *
     * @deprecated
     */
    @Deprecated
    private void assembleGraphcis()
    {
        // ASSUMPTION: this.nodes && this.graphics are not null or empty

        DesignerElement dElt;

        HashSet<DesignerColNode> dcns;
        for ( DesignerRowNode drn : this.nodes.keySet() )
        {
            dcns = this.nodes.get( drn );
            for ( DesignerColNode dcn : dcns )
            {
                /*
                 * Don't bother with nodes that don't contain cell data.
                 */
                if ( dcn.getDesignerCol().containsCellData() )
                {
                    if ( dcn.getDesignerCol().isJoinedHoriz() || dcn.
                            getDesignerCol().isJoinedVert() )
                    {
                        dElt = new DesignerElement( dcn.getDesignerCol() ,
                                this.nodes );
                    }
                    else
                    {
                        dElt = new DesignerElement( dcn.getDesignerCol() );
                    }
                    this.graphics.add( dElt );

                }
            }
        }
    }


    /**
     *
     * @deprecated
     */
    @Deprecated
    private void buildGraphicsList()
    {
        if ( this.nodes != null && this.nodes.isEmpty() == false )
        {
            if ( this.graphics == null )
            {
                /*
                 * Instantiate the shapes' list.
                 */
                this.graphics = new ArrayList<>();
            }
            //
            HashSet<DesignerColNode> dcns;
            for ( DesignerRowNode drn : this.nodes.keySet() )
            {
                dcns = this.nodes.get( drn );
                for ( DesignerColNode dcn : dcns )
                {
                    this.graphics.add( dcn.getDesignerCol() );
                }
            }
        }
    }


    /**
     * Clear and reset the graphical objects.
     */
    public void clear()
    {
        this.graphics.clear();
        this.graphics = null;
        this.graphics = new ArrayList<>();
        repaint();
    }


    /**
     * Converts a value from Fenestra's Fundamental Units to local scale.
     * <p/>
     * @param fu length in Fenestra's Fundamental Units.
     * @return equivalent value, scaled for local representation.
     */
    public static int convertFromFU( long fu )
    {
        return (int) ( fu / FU_SCALE_FACTOR );
    }


    /**
     *
     * @param g
     */
    private void drawGrid( Graphics2D g2d )
    {
        // step in DPI
        int step = 36;

//        int borderSize = this.getBounds().width
//                - this.getContentPane().getBounds().width;

        int borderSize = 0;

        int windowHeight = this.getBounds().height - borderSize;
        int windowWidth = this.getBounds().width - borderSize;

        if ( borderSize > 0 )
        {
            borderSize = ( borderSize / 2 );
        }

//        int titleBarHeight = windowHeight
//                - this.getContentPane().getBounds().height;
        int titleBarHeight = 0;

        g2d.setColor( this.bgColor );
        g2d.fillRect( 0 , 0 , windowWidth , windowHeight );

        if ( this.drawGrid )
        {
            // right now we'll just use light gray for the grid color, this can
            // change in the future if we need.
            g2d.setColor( Color.LIGHT_GRAY );

            // vertical lines
            for ( int i = borderSize + step; i < windowWidth; i += step )
            {
                g2d.
                        drawLine( i , borderSize + titleBarHeight , i ,
                        windowHeight );
            }

            // horizontal lines
            for ( int j = borderSize + step + titleBarHeight; j < windowHeight;
                    j += step )
            {
                g2d.drawLine( borderSize , j , windowWidth , j );
            }

            // set the color back to the background color so it will not affect
            // any other drawing function.
            g2d.setColor( this.bgColor );
        }

    }


    /**
     *
     * @param x
     * @param y
     * @return
     */
    private Node findNodeByCoords( int x , int y )
    {
        return this.
                findNodeByCoords( x * FU_SCALE_FACTOR , y * FU_SCALE_FACTOR );
    }


    /**
     *
     * @param x
     * @param y
     * @return
     */
    private Node findNodeByCoords( long x , long y )
    {
        int rowNumber = 0;
        int colNumber = 0;

        long width = 0;
        long height = 0;

        for ( Integer col : this.colWidths.keySet() )
        {
            width += this.colWidths.get( col );
            if ( x <= width )
            {
                colNumber = col;
                break;
            }
        }

        for ( Integer row : this.rowHeights.keySet() )
        {
            height += this.rowHeights.get( row );
            if ( y <= height )
            {
                rowNumber = row;
                break;
            }
        }

        //
        return this.findNodeAt( rowNumber , colNumber );
    }


    /**
     *
     * @param rowNumber
     * @param colNumber
     * @return
     */
    private Node findNodeAt( int rowNumber , int colNumber )
    {
        //
        // loop through some structure to find the node we want...
        //
        // <DesignerRowNode , HashSet<DesignerColNode>>
        //
        for ( DesignerRowNode drn : this.nodes.keySet() )
        {
            if ( drn.getRowNumber().equals( rowNumber ) )
            {
                for ( DesignerColNode dcn : this.nodes.get( drn ) )
                {
                    if ( dcn.getDesignerCol().getColNumber().equals( colNumber ) )
                    {
                        // return dcn;
                        Node node = dcn;
                        return node;
                    }
                }
            }
        }
        return null;
    }


    /**
     *
     * @return
     */
    public Color getColor()
    {
        return this.bgColor;
    }


    /**
     *
     * @return
     */
    public ArrayList<Drawable> getGraphicObjects()
    {
        return this.graphics;
    }


    /**
     *
     * @return true if the background grid is being drawn, false otherwise.
     */
    public boolean getGridVisible()
    {
        return this.drawGrid;
    }


    /**
     *
     * @return the minimum size required by the canvas.
     */
    @Override
    public Dimension getMinimumSize()
    {
        Dimension d = this.getPreferredSize();
        return new Dimension( d.width / 2 , d.height / 2 );
    }


    /**
     *
     * @param n
     * @return
     */
    public Node getNodeParent( Node n )
    {
        if ( n instanceof DesignerColNode )
        {
            DesignerColNode dcn = (DesignerColNode) n;
            Integer rowNumber = dcn.getRowNumber();

            for ( DesignerRowNode drn : this.nodes.keySet() )
            {
                if ( drn.getDesignerRow().getRowNumber().equals( rowNumber ) )
                {
                    return drn;
                }
            }
        }
        return null;
    }


    /**
     * The getPreferredSize() function is necessary for a scroll pane containing
     * the canvas to work.
     * <p/>
     * Without this function a scroll pane would not 'know' when the viewing
     * area is smaller than the content it needs to display, so it would not be
     * able to activate the scroll bars.
     * <p/>
     * @return - the preferred (or ideal) dimension the canvas needs.
     */
    @Override
    public Dimension getPreferredSize()
    {
        long height = 32;
        long width = 32;

        if ( this.colWidths != null && this.rowHeights != null )
        {
            for ( Integer h : this.rowHeights.keySet() )
            {
                height += this.rowHeights.get( h );
            }

            for ( Integer w : this.colWidths.keySet() )
            {
                width += this.colWidths.get( w );
            }
        }

        Dimension d = new Dimension( (int) ( width / Canvas.FU_SCALE_FACTOR ) ,
                (int) ( height / Canvas.FU_SCALE_FACTOR ) );
        return d;
    }


    /**
     *
     */
    public void mouseClick( int x , int y )
    {
        Node found = this.findNodeByCoords( x , y );
        if ( null != found )
        {
            Node[] selected =
            {
                found
            };
            try
            {
                this.topComponent.setSelectedTreeNodes( selected );
                this.setSelectedNodes( selected );
            }
            catch ( PropertyVetoException ex )
            {
                Exceptions.printStackTrace( ex );
            }
        }
        else
        {
            System.err.println(
                    this.getClass().getName()
                    + " No nodes found at " + x + " , " + y );
        }
    }


    /**
     *
     * @param g
     */
    @Override
    public void paint( Graphics g )
    {
        super.paint( g );

        // cast a copy of Graphics to Graphics2D to get functions that we need.
        Graphics2D g2d = (Graphics2D) g;

        // draw / fill the canvas' background.
        this.drawGrid( g2d );

        /*
         * Loop through all the (drawable) elements in the graphcs list and
         * 'tell' each one to draw itself on our canvas with the graphics
         * object.
         */
        for ( Drawable d : graphics )
        {
            /*
             * These objects all implement the Drawable interface, so all we
             * need to do is call 'draw( Grahpics2D g2d )' on each one.
             */
            d.draw( g2d );
        }
    }


    /**
     * Parses a string argument containing a measurement in Fundamental Units
     * and converts (scales) the value so it can be represented on the canvas.
     * <p/>
     * @param size a String containing a measurement in Fundamental Units.
     * @return the integer value represented by the argument
     * @throws NumberFormatException if the parameter cannot be parsed as a
     *                               number.
     */
    public static int parseFUSize( String size ) throws NumberFormatException
    {
        long fUnit = Long.parseLong( size.replace( Canvas.FU_UNIT_NAME , "" ) );
        return Canvas.convertFromFU( fUnit );
    }


    /**
     *
     * @param imageFileName
     * @throws IOException
     */
    public void writePreviewImage( String imageFileName ) throws IOException
    {
        //ImageWriter.jpegImageWriter(
        ImageWriter.pngImageWriter(
                imageFileName ,
                this.getPreferredSize() ,
                (ArrayList<Drawable>) this.graphics.clone() );
    }


    /**
     *
     * @param pdfFileName
     */
    public void writePreviewPdf( String pdfFileName )
    {
        try
        {
            //
            PdfFileWriter.printPdf(
                    pdfFileName ,
                    this.getPreferredSize() ,
                    graphics );
        }
        catch ( DocumentException | FileNotFoundException ex )
        {
            Exceptions.printStackTrace( ex );
        }
    }


    /**
     * Set the canvas' background color to the one passed in as parameter.
     * <p/>
     * @param c
     */
    public void setBackGroundColor( Color c )
    {
        this.bgColor = c;
        this.repaint();
    }


    /**
     * Toggle the border for all the elements on the canvas.
     * <p/>
     * @param visible
     */
    public void setBordersVisible( boolean visible )
    {
        /*
         * loop through all the rows and all the columns and set the border
         * visible property to whatever is passed in as parameter
         */
        for ( DesignerRowNode drn : this.nodes.keySet() )
        {
            for ( DesignerColNode dcn : this.nodes.get( drn ) )
            {
                dcn.setBorderVisible( visible );
            }
        }
        this.repaint();
    }


    /**
     * Toggle the background grid's visibility.
     * <p/>
     * @param visible
     */
    public void setGridVisible( boolean visible )
    {
        this.drawGrid = visible;
        this.repaint();
    }


    /**
     *
     * @param ck
     */
    public void setGrahpicObjects( Children.Keys ck )
    {
        if ( ck instanceof DesignerRowChildren )
        {
            DesignerRowChildren drc = (DesignerRowChildren) ck;

            // list of column widths
            this.colWidths = drc.getColWidths();
            // list of row heights
            this.rowHeights = drc.getRowHeights();
            // row -> column mapping
            this.nodes = drc.getNodeMapping();
            //
            buildGraphicsList();
        }
        else if ( ck instanceof DesignerElementChildren )
        {
            // row -> column mapping -- instantiated so stuff doesn't break.
            this.nodes = new HashMap<>();

            this.designerElements = (DesignerElementChildren) ck;

            // list of column widths
            this.colWidths = this.designerElements.getColWidths();
            // list of row heights
            this.rowHeights = this.designerElements.getRowHeights();
            //
            this.graphics = this.designerElements.getGraphics();
        }
    }


    /**
     *
     * @param shapes
     */
    public void setGraphicShapes( ArrayList<Drawable> shapes )
    {
        this.graphics = shapes;
        this.repaint();
    }


    /**
     * Set the nodes listed in the
     * <p/>
     * @param found array as selected.
     * <p/>
     * @param found
     */
    public void setSelectedNodes( Node[] found )
    {
        // fist de-select ALL nodes
        for ( DesignerRowNode drn : this.nodes.keySet() )
        {
            for ( DesignerColNode dcn : this.nodes.get( drn ) )
            {
                dcn.setSelected( false );
            }
        }

        // then select only those listed
        DesignerColNode dcn;
        for ( int i = 0; i < found.length; i++ )
        {
            if ( found[i] instanceof DesignerColNode )
            {
                dcn = (DesignerColNode) found[i];
                dcn.setSelected( true );
            }
            else
            {
                System.err.println(
                        this.getClass().getName() + ": the node " + found[i] + " cannot be cast to 'DesignerColNode'" );
            }
        }

        this.repaint();
    }

}
